﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using LaunchPad.Core.Maths;

namespace LaunchPad.Core.Controls
{
    internal enum SelectionMode
    {
        Single,
        Extended,
        Inverted
    }

    /// <summary>
    /// Interaction logic for MapControl.xaml
    /// </summary>
    public partial class MapControl : UserControl
    {
        public static readonly DependencyProperty NodesProperty = DependencyProperty.Register(
            "Nodes", typeof (ObservableCollection<Node>), typeof (MapControl),
            new PropertyMetadata(OnNodesChanged));

        public static readonly DependencyProperty EffectsProperty = DependencyProperty.Register(
            "Effects", typeof (ObservableCollection<Track>), typeof (MapControl),
            new PropertyMetadata(OnEffectsChanged));

        private readonly XPositionConverter _xConverter = new XPositionConverter();
        private readonly YPositionConverter _yConverter = new YPositionConverter();
        private Point _clickPos;
        private Dictionary<MapItemControl, Rect> _initialItemPositions;
        private List<IMappable> _selectedItems;

        public MapControl()
        {
            InitializeComponent();
            _xConverter.MapControl = this;
            _yConverter.MapControl = this;
            SizeChanged += MapControlSizeChanged;
            MouseUp += MapControlMouseUp;
            MouseLeftButtonDown += MapControlMouseLeftButtonDown;
            MouseMove += MapControlMouseMove;

            selectionRect.SizeChanged += SelectionRectSizeChanged;
        }

        public ObservableCollection<Node> Nodes
        {
            get { return (ObservableCollection<Node>) GetValue(NodesProperty); }
            set { SetValue(NodesProperty, value); }
        }

        public ObservableCollection<Track> Effects
        {
            get { return (ObservableCollection<Track>) GetValue(EffectsProperty); }
            set { SetValue(EffectsProperty, value); }
        }

        private SelectionMode SelectionMode { get; set; }

        private static void OnNodesChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var control = sender as MapControl;
            if (control != null)
                control.OnNodesChanged(e);
        }

        private static void OnEffectsChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var control = sender as MapControl;
            if (control != null)
                control.OnEffectsChanged(e);
        }

        protected void OnNodesChanged(DependencyPropertyChangedEventArgs e)
        {
            if (e.OldValue != null)
            {
                ((ObservableCollection<Node>) e.OldValue).CollectionChanged -=
                    NodesCollectionChanged;

                foreach (Node node in e.OldValue as ObservableCollection<Node>)
                {
                    Remove(node);
                }
            }

            if (e.NewValue != null)
            {
                ((ObservableCollection<Node>) e.NewValue).CollectionChanged +=
                    NodesCollectionChanged;

                foreach (Node node in e.NewValue as ObservableCollection<Node>)
                {
                    AddNode(node);
                }
            }
        }

        protected void OnEffectsChanged(DependencyPropertyChangedEventArgs e)
        {
            if (e.OldValue != null)
            {
                ((ObservableCollection<Track>) e.OldValue).CollectionChanged -=
                    EffectsCollectionChanged;

                IEnumerable<EffectTrack> effects = (e.OldValue as ObservableCollection<Track>).OfType<EffectTrack>();
                foreach (EffectTrack effect in effects)
                {
                    Remove(effect);
                }
            }

            if (e.NewValue != null)
            {
                ((ObservableCollection<Track>) e.NewValue).CollectionChanged +=
                    EffectsCollectionChanged;

                IEnumerable<EffectTrack> effects = (e.NewValue as ObservableCollection<Track>).OfType<EffectTrack>();
                foreach (EffectTrack effect in effects)
                {
                    AddEffect(effect);
                }
            }
        }

        private void NodesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (Node node in e.NewItems)
                    AddNode(node);
            }

            if (e.OldItems != null)
            {
                foreach (Node node in e.OldItems)
                    Remove(node);
            }
        }

        private void EffectsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (EffectTrack effect in e.NewItems.OfType<EffectTrack>())
                    AddEffect(effect);
            }

            if (e.OldItems != null)
            {
                foreach (EffectTrack effect in e.OldItems.OfType<EffectTrack>())
                    Remove(effect);
            }
        }

        private void AddNode(Node node)
        {
            var nodeControl = new MapNodeControl();
            node.IsVisible = nodeVisibilityCheckBox.IsChecked.Value;
            AddMappable(node, nodeControl);
        }

        private void AddEffect(EffectTrack effect)
        {
            var effectControl = new MapEffectControl();
            AddMappable(effect, effectControl);
        }

        private void AddMappable(IMappable item, MapItemControl control)
        {
            var xBinding = new Binding("Location.X");
            var yBinding = new Binding("Location.Y");

            xBinding.Converter = _xConverter;
            yBinding.Converter = _yConverter;

            xBinding.Mode = yBinding.Mode = BindingMode.TwoWay;

            control.SetBinding(MapItemControl.PositionXProperty, xBinding);
            control.SetBinding(MapItemControl.PositionYProperty, yBinding);

            var visibilityBinding = new Binding("IsVisible") {Converter = new VisibilityConverter()};
            control.SetBinding(VisibilityProperty, visibilityBinding);

            mapCanvas.Children.Add(control);
            control.DataContext = item;

            control.Selected += ItemSelected;
            control.PositionChanging += ItemPositionChanging;
            control.PositionChanged += ItemPositionChanged;

            if (item is EffectTrack)
            {
                /* Add line to node */
                var line = new Line();
                line.SetValue(Panel.ZIndexProperty, -1);
                line.Stroke = new SolidColorBrush(Colors.Red);
                line.StrokeThickness = 1;

                var nodeXBinding = new Binding("Node.Location.X");
                var nodeYBinding = new Binding("Node.Location.Y");

                nodeXBinding.Converter = _xConverter;
                nodeYBinding.Converter = _yConverter;

                var nodeVisibilityBinding = new Binding("Node.IsVisible")
                                                {
                                                    FallbackValue = Visibility.Collapsed,
                                                    Converter = new VisibilityConverter()
                                                };

                line.SetBinding(Line.X1Property, xBinding);
                line.SetBinding(Line.Y1Property, yBinding);
                line.SetBinding(Line.X2Property, nodeXBinding);
                line.SetBinding(Line.Y2Property, nodeYBinding);
                line.SetBinding(VisibilityProperty, nodeVisibilityBinding);

                line.DataContext = item;
                mapCanvas.Children.Add(line);
            }

            control.PositionX = Math.Max(control.PositionX, 0);
            control.PositionY = Math.Max(control.PositionY, 0);
        }

        private void ItemPositionChanging(object sender, EventArgs e)
        {
            _initialItemPositions = GetItemControls().
                Where(control => ((IMappable) control.DataContext).IsSelected).
                ToDictionary(control => control, control => control.Rect);
        }

        private static Vector UncontainedBy(Rect rect, Rect container)
        {
            Vector v = default(Vector);
            if (rect.Left < container.Left)
                v.X = container.Left - rect.Left;
            if (rect.Top < container.Top)
                v.Y = container.Top - rect.Top;
            if (rect.Right > container.Right)
                v.X = container.Right - rect.Right;
            if (rect.Bottom > container.Bottom)
                v.Y = container.Bottom - rect.Bottom;
            return v;
        }


        private void ItemPositionChanged(object sender, MapItemControl.PositionChangedEventArgs e)
        {
            if (_initialItemPositions != null && _initialItemPositions.Any())
            {
                var mapRect = new Rect(0, 0, mapCanvas.ActualWidth, mapCanvas.ActualHeight);

                Vector translate = e.Distance;

                Rect transformedRect = _initialItemPositions.
                    Select(pair => Rect.Offset(pair.Value, translate)).
                    Aggregate((union, rect) => Rect.Union(rect, union));

                if (!mapRect.Contains(transformedRect))
                {
                    translate = Vector.Add(translate, UncontainedBy(transformedRect, mapRect));
                }

                /* Calculate transformed items */
                Dictionary<MapItemControl, Rect> transformed = _initialItemPositions.ToDictionary(
                    pair => pair.Key,
                    pair => Rect.Offset(pair.Value, translate));

                /* Make sure we don't leave map rect */
                if (transformed.All(pair => mapRect.Contains(pair.Value)))
                {
                    foreach (var pair in transformed)
                    {
                        pair.Key.PositionX = pair.Value.X + pair.Value.Width/2;
                        pair.Key.PositionY = pair.Value.Y + pair.Value.Height/2;
                    }
                }
            }
        }

        private void SetSelectionMode()
        {
            if (Keyboard.Modifiers.HasFlag(ModifierKeys.Control))
            {
                SelectionMode = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) ? SelectionMode.Inverted : SelectionMode.Extended;
            }
            else
            {
                SelectionMode = SelectionMode.Single;
            }
        }

        private void ItemSelected(object sender, MouseButtonEventArgs e)
        {
            var control = (MapItemControl) sender;
            var selectedMappable = (IMappable)control.DataContext;

            SetSelectionMode();

            switch (e.ClickCount)
            {
                case 1:
                    if (SelectionMode == SelectionMode.Extended)
                    {
                        selectedMappable.IsSelected = !selectedMappable.IsSelected;
                    }
                    else
                    {
                        foreach (IMappable mappable in GetMappableItems())
                            if (mappable == selectedMappable)
                                mappable.IsSelected = !mappable.IsSelected;
                            else mappable.IsSelected = false;
                    }
                    break;
                    /* For doubleclicks, select associated effects as well */
                case 2:
                    {
                        if (sender is MapNodeControl)
                        {
                            var node = (Node)control.DataContext;

                            foreach (EffectTrack effect in node.Effects)
                                effect.IsSelected = true;
                        }
                    }
                    break;
            }
        }

        private void Remove(IMappable item)
        {
            List<MapItemControl> controls = GetItemControls().Where(c => c.DataContext == item).ToList();

            foreach (MapItemControl control in controls)
                mapCanvas.Children.Remove(control);
        }

        private IEnumerable<UIElement> GetMapControls()
        {
            if (mapCanvas == null)
                return Enumerable.Empty<UIElement>();
            return mapCanvas.Children.Cast<UIElement>();
        }

        private IEnumerable<MapItemControl> GetItemControls()
        {
            return GetMapControls().OfType<MapItemControl>();
        }

        private IEnumerable<IMappable> GetMappableItems()
        {
            return GetItemControls().Select(item => item.DataContext).Cast<IMappable>();
        }

        private IEnumerable<Line> GetNodeLines()
        {
            return GetMapControls().OfType<Line>().Where(line => line.DataContext is EffectTrack);
        }


        private void MapControlSizeChanged(object sender, SizeChangedEventArgs e)
        {
            foreach (MapItemControl item in GetItemControls())
            {
                item.UpdateBindings();
            }

            var properties = new[]
                                 {Line.X1Property, Line.X2Property, Line.Y1Property, Line.Y2Property};
            foreach (Line line in GetNodeLines())
            {
                foreach (DependencyProperty property in properties)
                {
                    BindingExpression expression = line.GetBindingExpression(property);
                    if (expression != null)
                        expression.UpdateTarget();
                }
            }
        }

        private void UnselectAll()
        {
            foreach (IMappable item in GetMappableItems())
                item.IsSelected = false;
        }

        private void MapControlMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            SetSelectionMode();

            if (e.LeftButton == MouseButtonState.Pressed)
            {
                _clickPos = e.GetPosition(mapCanvas);

                selectionRect.Visibility = Visibility.Visible;

                if (SelectionMode == SelectionMode.Single)
                    UnselectAll();

                _selectedItems = GetMappableItems().Where(item => item.IsSelected).ToList();

                CaptureMouse();

                e.Handled = true;
            }
        }

        private void MapControlMouseUp(object sender, MouseButtonEventArgs e)
        {
            if (IsMouseCaptured)
                ReleaseMouseCapture();
            selectionRect.Visibility = Visibility.Hidden;
        }

        private void MapControlMouseMove(object sender, MouseEventArgs e)
        {
            if (IsMouseCaptured)
            {
                Point pos = e.GetPosition(mapCanvas);

                // constrain to box
                pos = new Point(Math.Max(pos.X, 0), Math.Max(pos.Y, 0));
                double x = Math.Min(pos.X, ActualWidth);
                double y = Math.Min(pos.Y, ActualHeight);

                double width = x - _clickPos.X;
                double height = y - _clickPos.Y;

                var origin = new Point(width < 0 ? pos.X : _clickPos.X, height < 0 ? pos.Y : _clickPos.Y);
                selectionRect.SetValue(Canvas.LeftProperty, origin.X);
                selectionRect.SetValue(Canvas.TopProperty, origin.Y);

                selectionRect.SetValue(WidthProperty, Math.Abs(width));
                selectionRect.SetValue(HeightProperty, Math.Abs(height));

                e.Handled = true;
            }
        }

        private void SelectionRectSizeChanged(object sender, SizeChangedEventArgs e)
        {
            // look for node collisions with selection rect
            var x = (double) selectionRect.GetValue(Canvas.LeftProperty);
            var y = (double) selectionRect.GetValue(Canvas.TopProperty);

            var selectPos = new Point(x, y);
            var selectRect = new Rect(selectPos, e.NewSize);

            foreach (MapItemControl control in GetItemControls())
            {
                var item = (IMappable)control.DataContext;
                bool intersects = control.Rect.IntersectsWith(selectRect);

                switch (SelectionMode)
                {
                    case SelectionMode.Single:
                        item.IsSelected = intersects;
                        break;
                    case SelectionMode.Extended:
                        item.IsSelected = intersects || _selectedItems.Contains(item);
                        break;
                    case SelectionMode.Inverted:
                        item.IsSelected = !intersects && _selectedItems.Contains(item);
                        break;
                }
            }
        }

        private void NodeVisibilityCheckBoxChecked(object sender, RoutedEventArgs e)
        {
            SetNodeVisibility(true);
        }

        private void NodeVisibilityCheckBoxUnchecked(object sender, RoutedEventArgs e)
        {
            SetNodeVisibility(false);
        }

        private void SetNodeVisibility(bool visible)
        {
            if (Nodes == null)
                return;

            foreach (Node node in Nodes)
                node.IsVisible = visible;
        }

        private void MapCanvasMouseWheel(object sender, MouseWheelEventArgs e)
        {
            //Point pos = e.GetPosition(mapCanvas);
            //mapTranslateTransform.X = pos.X;
            //mapTranslateTransform.Y = pos.Y;

            //double zoomFactor = 1 + (double)e.Delta / 400;
            //zoomFactor = Math.Max(zoomFactor, 0.25);
            //zoomFactor = Math.Min(zoomFactor, 2);
            //double scale = mapScaleTransform.ScaleX * zoomFactor;
            //scale = Math.Max(1, scale);


            //mapScaleTransform.ScaleX = scale;
            //mapScaleTransform.ScaleY = scale;
        }

        private void AutoplaceButtonClick(object sender, RoutedEventArgs e)
        {
            AssignNearest();
        }

        private void AssignNearest()
        {
            var activeNodes = Nodes.Where(node => node.ConnectionState == ConnectionState.Connected).ToList();

            if (!activeNodes.Any())
                return;

            IEnumerable<Cluster<EffectTrack>> effectClusters =
                Effects.OfType<EffectTrack>().Cluster(i => i.Location.ToPoint(), activeNodes.Count, Node.MaxEffects);

            int centerNumber = 0;
            foreach (var effectCluster in effectClusters)
            {
                var node = activeNodes[centerNumber];
                node.Effects.Clear();
                node.Location = new ObservablePoint(effectCluster.Centroid.X, effectCluster.Centroid.Y);

                foreach (EffectTrack effect in effectCluster)
                    node.Effects.Add(effect);

                centerNumber++;
            }
        }
    }

    public class XPositionConverter : IValueConverter
    {
        private const int MaxInput = 100;

        public XPositionConverter()
        {
        }

        public XPositionConverter(MapControl control) :
            this(control, 0)
        {
        }

        public XPositionConverter(MapControl control, double offset)
        {
            MapControl = control;
            Offset = offset;
        }

        public MapControl MapControl { get; set; }
        public double Offset { get; set; }

        #region IValueConverter Members

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var percent = (double) value;
            return percent*MapControl.ActualWidth/MaxInput;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (MapControl.ActualWidth == 0)
                return 0;

            var x = (double) value;
            return x*MaxInput/MapControl.ActualWidth;
        }

        #endregion
    }

    public class YPositionConverter : IValueConverter
    {
        private const int MaxInput = 100;

        public YPositionConverter()
        {
        }

        public YPositionConverter(MapControl control) :
            this(control, 0)
        {
        }

        public YPositionConverter(MapControl control, double offset)
        {
            MapControl = control;
            Offset = offset;
        }

        public MapControl MapControl { get; set; }
        public double Offset { get; set; }

        #region IValueConverter Members

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var percent = (double) value;
            return percent*MapControl.ActualHeight/MaxInput;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (MapControl.ActualHeight == 0)
                return 0;

            var y = (double) value;
            return y*MaxInput/MapControl.ActualHeight;
        }

        #endregion
    }
}